Popular Searches
Popular Course Categories
Popular Courses

Testing Components Using React Native Testing Library

Testing Components Using React Native Testing Library

Testing and Debugging React Native Applications

Testing Components Using React Native Testing Library

Course: React Native Training

Topic: Testing Components Using React Native Testing Library

Course Link: React Native Training at JustAcademy

Demo Link: Register for a Free Course Demo

Website: JustAcademy


1. Introduction to Component Testing

Component testing is an important part of React Native application development. It verifies that individual UI components render correctly, respond to user interactions, display appropriate messages, and behave as expected under different conditions.

React Native applications contain components such as buttons, text inputs, forms, navigation menus, product cards, modals, profile screens, and lists. Testing these components helps developers identify problems before the application reaches users.

React Native Testing Library (RNTL) is a testing library designed to test React Native components from the user's perspective. It provides utilities to render components, find elements, simulate interactions, and verify visible results.

React Native Testing Library is commonly used with Jest, a JavaScript testing framework. Jest runs tests, provides assertions, creates mock functions, and reports test results.

1.1 Why Component Testing Is Important

  • Reliability: Confirms that components behave as expected.
  • Bug detection: Identifies errors during development.
  • Regression prevention: Helps detect when new code breaks existing functionality.
  • Maintainability: Makes refactoring safer.
  • User experience: Checks visible content and common user interactions.
  • Team collaboration: Documents expected behavior through executable tests.

1.2 Example of a Component That Needs Testing

Consider a login screen containing an email field, a password field, and a login button. Tests can verify that the fields appear, the user can enter information, validation messages are displayed, and a successful submission triggers the expected behavior.

2. What Is React Native Testing Library?

React Native Testing Library provides tools for rendering components in a JavaScript testing environment and interacting with their rendered output. It encourages tests that resemble how people use an application rather than tests that depend heavily on internal component state or implementation details.

2.1 Main Features

  • render() renders a React Native component for testing.
  • screen provides queries for locating rendered elements.
  • getByRole() finds elements by accessibility role and accessible name.
  • getByText() finds elements containing specific text.
  • getByPlaceholderText() locates inputs by their placeholder.
  • userEvent simulates common user interactions such as pressing and typing.
  • fireEvent triggers supported events when needed.
  • findBy... queries wait for elements to appear asynchronously.
  • waitFor() waits for an assertion or condition to become true.
  • Jest matchers help verify rendered output and behavior.

2.2 React Native Testing Library vs. Jest

Feature React Native Testing Library Jest
Main purpose Render and interact with React Native components. Run tests and evaluate assertions.
Element queries Provides queries for rendered UI. Does not provide RNTL's component queries by itself.
User interactions Provides user-event and event utilities. Provides mock functions and timer utilities.
Assertions Includes matchers for React Native rendered output. Provides general-purpose assertions such as toBe and toEqual.
Relationship Works alongside Jest in a typical project. Acts as the test runner and assertion framework.

3. Setting Up the Testing Environment

Before writing component tests, ensure that the React Native project has a compatible test environment. React Native project templates commonly include Jest configuration, but the exact setup depends on the React Native version and project tooling.

3.1 Install the Required Packages

For a compatible React Native project, install React Native Testing Library as a development dependency:

npm install --save-dev @testing-library/react-native

React Native Testing Library versions have different compatibility requirements. Follow the installation instructions for the version used by your project. For versions that require react-test-renderer, its version should match the installed React version. Check the current official documentation before installing or upgrading dependencies.

3.2 Configure Jest

A React Native CLI project may use a Jest configuration similar to the following. The correct preset depends on the project's React Native version.

// jest.config.js
module.exports = {
  preset: 'react-native',
  testMatch: [
    '**/__tests__/**/*.test.js',
    '**/?(*.)+(spec|test).js'
  ]
};

Some newer React Native setups use a separately installed Jest preset. Use the preset recommended by the version of React Native in your project rather than copying a configuration blindly.

3.3 Create a Test File

Test files are often stored in a __tests__ directory or beside the component they test.

src/
  components/
    WelcomeMessage.js
    WelcomeMessage.test.js
  screens/
    LoginScreen.js
    LoginScreen.test.js

3.4 Run the Tests

npm test

You can also use the test script configured in package.json, for example:

{
  "scripts": {
    "test": "jest"
  }
}

Running the test command executes the test files discovered by Jest and reports which tests passed or failed.

4. Understanding the Basic Testing Workflow

A typical component test follows a repeatable workflow: render the component, find an element, interact with it if necessary, and verify the resulting behavior.

  1. Arrange: Prepare the component and any required props or mocks.
  2. Render: Render the component using React Native Testing Library.
  3. Locate: Find the UI element using a query.
  4. Act: Perform a user interaction when required.
  5. Assert: Check the expected output or behavior.

4.1 Example: Testing a Welcome Component

First, create the component:

// WelcomeMessage.js
import React from 'react';
import { View, Text } from 'react-native';

export default function WelcomeMessage() {
  return (
    <View>
      <Text>Welcome to React Native!</Text>
    </View>
  );
}

Next, write the test:

// WelcomeMessage.test.js
import React from 'react';
import { render, screen } from '@testing-library/react-native';
import WelcomeMessage from './WelcomeMessage';

test('displays the welcome message', () => {
  render(<WelcomeMessage />);

  expect(
    screen.getByText('Welcome to React Native!')
  ).toBeOnTheScreen();
});

Explanation: The test renders the component and verifies that the welcome message is present. It tests the user-visible result rather than checking how the component is implemented internally.

5. Using render() to Render Components

The render() function mounts a component in the testing environment. Once rendered, the test can inspect the component's output using queries and interact with its elements.

5.1 Rendering a Component with Props

// Greeting.js
import React from 'react';
import { Text } from 'react-native';

export default function Greeting({ name }) {
  return <Text>Hello, {name}!</Text>;
}
// Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react-native';
import Greeting from './Greeting';

test('displays the supplied name', () => {
  render(<Greeting name="Asha" />);

  expect(screen.getByText('Hello, Asha!'))
    .toBeOnTheScreen();
});

5.2 Testing Different Prop Values

When a component's output depends on props, test representative values, including normal values, empty values, and boundary conditions where appropriate.

test('displays a different greeting for another name', () => {
  render(<Greeting name="Rahul" />);

  expect(screen.getByText('Hello, Rahul!'))
    .toBeOnTheScreen();
});

5.3 Rendering with Context Providers

Components that use context may need a provider during testing. For example, a component that reads a theme from a theme context should be rendered inside that provider.

import React from 'react';
import { render, screen } from '@testing-library/react-native';
import { ThemeContext } from './ThemeContext';
import ThemedLabel from './ThemedLabel';

test('renders inside a theme provider', () => {
  render(
    <ThemeContext.Provider value={{ theme: 'dark' }}>
      <ThemedLabel />
    </ThemeContext.Provider>
  );

  expect(screen.getByText('Dark mode'))
    .toBeOnTheScreen();
});

The provider and expected text in this example represent an application's own implementation. Adjust them to match the actual context API and component behavior.

6. Finding Elements with Queries

Queries locate elements in the rendered component tree. Choosing an appropriate query makes tests easier to read and less fragile.

6.1 Common Query Types

Query Purpose Typical use
getByRole() Finds an element by accessibility role and optional accessible name. Buttons and accessible controls.
getByText() Finds an element by visible text. Headings, labels, and messages.
getByLabelText() Finds an element using its accessibility label. Accessible form controls.
getByPlaceholderText() Finds an input by placeholder text. Text input fields.
getByTestId() Finds an element by a test identifier. When a meaningful user-facing query is not practical.
queryByText() Returns a match or null if none exists. Checking that content is absent.
findByText() Waits for matching text to appear. Content that appears after asynchronous work.

6.2 Prefer Accessibility Queries

Whenever practical, use queries based on accessible roles and names. These queries encourage accessible components and express how a user or assistive technology identifies a control.

import React from 'react';
import { Button, View } from 'react-native';

export default function SaveButton({ onSave }) {
  return (
    <View>
      <Button title="Save profile" onPress={onSave} />
    </View>
  );
}
import React from 'react';
import { render, screen } from '@testing-library/react-native';
import SaveButton from './SaveButton';

test('shows the save button', () => {
  render(<SaveButton onSave={() => {}} />);

  expect(
    screen.getByRole('button', { name: 'Save profile' })
  ).toBeOnTheScreen();
});

6.3 When to Use Test IDs

Use testID only when a useful role, label, placeholder, or text query is unavailable or unsuitable. Overusing test IDs can make tests depend on implementation details rather than the UI that users experience.

<View testID="profile-summary">
  <Text>Account overview</Text>
</View>
expect(screen.getByTestId('profile-summary'))
  .toBeOnTheScreen();

6.4 Understanding getBy, queryBy, and findBy

  • getBy... expects a matching element to exist immediately. It throws an error if no match exists or if the query is ambiguous.
  • queryBy... returns null when no matching element exists. It is useful for negative assertions.
  • findBy... returns a promise and waits for an element to appear. It is useful for asynchronous updates.
// Element should already exist
expect(screen.getByText('Profile')).toBeOnTheScreen();

// Element should not exist
expect(screen.queryByText('Invalid email'))
  .not.toBeOnTheScreen();

// Element appears after an asynchronous update
expect(await screen.findByText('Profile loaded'))
  .toBeOnTheScreen();

7. Testing Button Interactions

Buttons commonly trigger navigation, form submission, saving, deleting, or updating data. Button tests should verify the outcome of pressing the button, not simply that a callback exists.

7.1 Testing a Button with a Callback

// ActionButton.js
import React from 'react';
import { Button } from 'react-native';

export default function ActionButton({ onAction }) {
  return (
    <Button title="Continue" onPress={onAction} />
  );
}
// ActionButton.test.js
import React from 'react';
import { render, screen, userEvent } from '@testing-library/react-native';
import ActionButton from './ActionButton';

test('calls the action when pressed', async () => {
  const onAction = jest.fn();
  const user = userEvent.setup();

  render(<ActionButton onAction={onAction} />);

  await user.press(
    screen.getByRole('button', { name: 'Continue' })
  );

  expect(onAction).toHaveBeenCalledTimes(1);
});

Explanation: jest.fn() creates a mock function. The test presses the button and verifies that the callback is called exactly once.

7.2 Testing a Disabled Button

Disabled controls should not perform actions. Test both the disabled state and the expected behavior when the control is unavailable.

import React from 'react';
import { Button } from 'react-native';

export default function SubmitButton({ disabled, onSubmit }) {
  return (
    <Button
      title="Submit"
      disabled={disabled}
      onPress={onSubmit}
    />
  );
}
import React from 'react';
import { render, screen, userEvent } from '@testing-library/react-native';
import SubmitButton from './SubmitButton';

test('disables submission when required', async () => {
  const onSubmit = jest.fn();
  const user = userEvent.setup();

  render(<SubmitButton disabled onSubmit={onSubmit} />);

  expect(
    screen.getByRole('button', { name: 'Submit' })
  ).toBeDisabled();

  // Do not attempt to force a user interaction on a disabled control.
  expect(onSubmit).not.toHaveBeenCalled();
});

Make sure the disabled matcher is supported by the installed library version and that the component exposes its disabled state correctly.

8. Testing TextInput Components

Text inputs are used in login forms, registration screens, search bars, feedback forms, and profile settings. Tests should verify that input values can be entered and that the application responds correctly to them.

8.1 Example Input Component

// NameForm.js
import React, { useState } from 'react';
import { View, Text, TextInput, Button } from 'react-native';

export default function NameForm() {
  const [name, setName] = useState('');
  const [submittedName, setSubmittedName] = useState('');

  return (
    <View>
      <TextInput
        accessibilityLabel="Your name"
        placeholder="Enter your name"
        value={name}
        onChangeText={setName}
      />
      <Button
        title="Submit"
        onPress={() => setSubmittedName(name.trim())}
      />
      {submittedName !== '' && (
        <Text>Hello, {submittedName}!</Text>
      )}
    </View>
  );
}

8.2 Testing Input and Submission

import React from 'react';
import { render, screen, userEvent } from '@testing-library/react-native';
import NameForm from './NameForm';

test('submits the entered name', async () => {
  const user = userEvent.setup();

  render(<NameForm />);

  await user.type(
    screen.getByLabelText('Your name'),
    'Asha'
  );

  await user.press(
    screen.getByRole('button', { name: 'Submit' })
  );

  expect(screen.getByText('Hello, Asha!'))
    .toBeOnTheScreen();
});

This test follows the user's workflow: entering text, pressing Submit, and observing the greeting. If the installed RNTL version or component configuration differs, use the matching documented interaction API.

8.3 Testing Empty Input

Empty input is a common edge case. If a form requires a name, test that submission does not produce a successful result when the field is empty.

test('does not show a greeting for empty input', async () => {
  const user = userEvent.setup();

  render(<NameForm />);

  await user.press(
    screen.getByRole('button', { name: 'Submit' })
  );

  expect(screen.queryByText('Hello, !'))
    .not.toBeOnTheScreen();
});

For a production form, the component should ideally display a meaningful validation message. Test that message explicitly when it is part of the expected behavior.

9. Testing Form Validation

Form validation prevents incomplete or invalid data from being submitted. Common checks include required fields, email formats, minimum password length, matching passwords, and accepted terms.

9.1 Example: Email Validation

// EmailForm.js
import React, { useState } from 'react';
import { View, Text, TextInput, Button } from 'react-native';

export default function EmailForm() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = () => {
    const valid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

    if (!valid) {
      setError('Enter a valid email address');
      return;
    }

    setError('');
  };

  return (
    <View>
      <TextInput
        accessibilityLabel="Email address"
        placeholder="[email protected]"
        keyboardType="email-address"
        autoCapitalize="none"
        value={email}
        onChangeText={setEmail}
      />
      <Button title="Continue" onPress={handleSubmit} />
      {error !== '' && <Text>{error}</Text>}
    </View>
  );
}

9.2 Testing Invalid Email

import React from 'react';
import { render, screen, userEvent } from '@testing-library/react-native';
import EmailForm from './EmailForm';

test('shows an error for an invalid email', async () => {
  const user = userEvent.setup();

  render(<EmailForm />);

  await user.type(
    screen.getByLabelText('Email address'),
    'invalid-email'
  );

  await user.press(
    screen.getByRole('button', { name: 'Continue' })
  );

  expect(
    screen.getByText('Enter a valid email address')
  ).toBeOnTheScreen();
});

9.3 Testing Valid Email

Also test valid input. A form should not show the invalid-email error after valid data is submitted.

test('does not show the email error for valid input', async () => {
  const user = userEvent.setup();

  render(<EmailForm />);

  await user.type(
    screen.getByLabelText('Email address'),
    '[email protected]'
  );

  await user.press(
    screen.getByRole('button', { name: 'Continue' })
  );

  expect(
    screen.queryByText('Enter a valid email address')
  ).not.toBeOnTheScreen();
});

10. Testing Conditional Rendering

Conditional rendering displays different UI elements based on props, state, or application data. Examples include loading indicators, error messages, success banners, empty-state screens, and authenticated content.

10.1 Example Component

// StatusMessage.js
import React from 'react';
import { Text, View } from 'react-native';

export default function StatusMessage({ loading, error, data }) {
  if (loading) {
    return (
      <View>
        <Text>Loading...</Text>
      </View>
    );
  }

  if (error) {
    return (
      <View>
        <Text>Unable to load data</Text>
      </View>
    );
  }

  return (
    <View>
      <Text>{data ? 'Data loaded' : 'No data available'}</Text>
    </View>
  );
}

10.2 Testing the Loading State

test('shows a loading message', () => {
  render(
    <StatusMessage loading error={false} data={null} />
  );

  expect(screen.getByText('Loading...'))
    .toBeOnTheScreen();
});

10.3 Testing the Error State

test('shows an error message', () => {
  render(
    <StatusMessage loading={false} error data={null} />
  );

  expect(screen.getByText('Unable to load data'))
    .toBeOnTheScreen();
});

10.4 Testing the Empty State

test('shows an empty-state message', () => {
  render(
    <StatusMessage loading={false} error={false} data={null} />
  );

  expect(screen.getByText('No data available'))
    .toBeOnTheScreen();
});

11. Testing Asynchronous Behavior

Many components perform asynchronous work, such as fetching data, loading user profiles, submitting forms, or waiting for a response from an API. Tests should wait for the relevant UI change rather than assuming it appears immediately.

11.1 Using findBy Queries

Use a findBy... query when an element is expected to appear after asynchronous work. The query retries for a limited time before failing.

test('displays the result after loading', async () => {
  render(<StatusMessage loading={false} error={false} data="Ready" />);

  expect(await screen.findByText('Data loaded'))
    .toBeOnTheScreen();
});

The example above renders a final state directly. In a real asynchronous test, trigger the action that starts the request and wait for the resulting UI.

11.2 Testing an API-Driven Component with a Mock

Network requests should usually be isolated from component tests. A mock API function makes the result predictable and avoids relying on the internet or a live server.

// UserGreeting.js
import React, { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import { fetchUser } from './userApi';

export default function UserGreeting() {
  const [user, setUser] = useState(null);
  const [error, setError] = useState(false);

  useEffect(() => {
    let active = true;

    fetchUser()
      .then(result => {
        if (active) setUser(result);
      })
      .catch(() => {
        if (active) setError(true);
      });

    return () => {
      active = false;
    };
  }, []);

  if (error) return <Text>Unable to load user</Text>;
  if (!user) return <Text>Loading user...</Text>;

  return (
    <View>
      <Text>Hello, {user.name}</Text>
    </View>
  );
}
// UserGreeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react-native';
import UserGreeting from './UserGreeting';
import { fetchUser } from './userApi';

jest.mock('./userApi', () => ({
  fetchUser: jest.fn()
}));

test('displays the fetched user name', async () => {
  fetchUser.mockResolvedValue({ name: 'Asha' });

  render(<UserGreeting />);

  expect(await screen.findByText('Hello, Asha'))
    .toBeOnTheScreen();
});

test('displays an error when fetching fails', async () => {
  fetchUser.mockRejectedValue(new Error('Network error'));

  render(<UserGreeting />);

  expect(await screen.findByText('Unable to load user'))
    .toBeOnTheScreen();
});

Important: These tests illustrate the intended pattern. Configure mocks and cleanup according to the project's Jest setup, and ensure asynchronous work does not update unmounted components.

11.3 Using waitFor

waitFor() is useful when a condition or assertion must become true after asynchronous work. Prefer a findBy... query when simply waiting for an element to appear.

import { waitFor } from '@testing-library/react-native';

await waitFor(() => {
  expect(fetchUser).toHaveBeenCalledTimes(1);
});

Keep the callback focused on assertions. Do not place user interactions or other side effects inside a waitFor() callback because the callback may run multiple times.

12. Using userEvent and fireEvent

React Native Testing Library supports utilities for simulating user interactions. Prefer userEvent for common interactions because it more closely models the sequence of events associated with user actions. Use fireEvent when a particular event is not supported by the user-event API or when you need to invoke a specific handler.

12.1 Common userEvent Operations

Operation Purpose
user.press(element) Simulates pressing an interactive element.
user.type(input, text) Simulates typing into a text input.
user.clear(input) Clears the input value.
user.longPress(element) Simulates a long press when supported.
const user = userEvent.setup();

await user.type(
  screen.getByPlaceholderText('Search products'),
  'Headphones'
);

await user.press(
  screen.getByRole('button', { name: 'Search' })
);

12.2 Using fireEvent

fireEvent provides shortcuts such as fireEvent.press() and fireEvent.changeText(). Exact synchronous or asynchronous behavior can vary by library version, so consult the documentation matching your installed version.

import { fireEvent } from '@testing-library/react-native';

fireEvent.changeText(
  screen.getByPlaceholderText('Search products'),
  'Headphones'
);

fireEvent.press(
  screen.getByRole('button', { name: 'Search' })
);

12.3 Choosing the Right Interaction API

  • Use userEvent for typical user actions such as pressing and typing.
  • Use fireEvent when a specific event is not covered by userEvent.
  • Await asynchronous user-event operations.
  • Follow the API behavior documented for your installed RNTL version.

13. Testing Lists and Repeated Components

React Native applications frequently display arrays of data using FlatList, SectionList, or mapped components. Tests should verify that the relevant items appear and that interactions affect the correct item.

13.1 Example Product List

// ProductList.js
import React from 'react';
import { View, Text, FlatList } from 'react-native';

export default function ProductList({ products }) {
  return (
    <FlatList
      data={products}
      keyExtractor={item => item.id}
      renderItem={({ item }) => (
        <View>
          <Text>{item.name}</Text>
          <Text>${item.price}</Text>
        </View>
      )}
    />
  );
}

13.2 Testing the List Output

test('renders product names and prices', () => {
  const products = [
    { id: '1', name: 'Notebook', price: 5 },
    { id: '2', name: 'Pen', price: 2 }
  ];

  render(<ProductList products={products} />);

  expect(screen.getByText('Notebook')).toBeOnTheScreen();
  expect(screen.getByText('$5')).toBeOnTheScreen();
  expect(screen.getByText('Pen')).toBeOnTheScreen();
  expect(screen.getByText('$2')).toBeOnTheScreen();
});

13.3 Testing Empty Lists

If the application has an empty-list message, verify that it appears when the data array is empty. The component must implement the empty state for this test to pass.

test('handles an empty product list', () => {
  render(<ProductList products={[]} />);

  expect(screen.queryByText('Notebook'))
    .not.toBeOnTheScreen();
});

14. Testing Navigation-Related Components

Navigation tests verify the behavior of buttons and links that move users between screens. Component tests can check that the expected navigation callback is triggered. Full navigation flows may require additional integration tests using the actual navigator.

14.1 Mocking a Navigation Callback

// ProfileButton.js
import React from 'react';
import { Button } from 'react-native';

export default function ProfileButton({ onOpenProfile }) {
  return (
    <Button
      title="View profile"
      onPress={onOpenProfile}
    />
  );
}
test('requests navigation to the profile screen', async () => {
  const onOpenProfile = jest.fn();
  const user = userEvent.setup();

  render(<ProfileButton onOpenProfile={onOpenProfile} />);

  await user.press(
    screen.getByRole('button', { name: 'View profile' })
  );

  expect(onOpenProfile).toHaveBeenCalledTimes(1);
});

This verifies the component's interaction contract, not the actual navigation stack. To test a real navigation flow, render the relevant navigator with appropriate test routes and providers.

15. Testing Modals and Dialogs

Modals and dialogs display content above the main screen. Tests should verify that a modal opens when requested, displays its message and actions, and closes or performs the appropriate action when a button is pressed.

15.1 Example Confirmation Dialog

// DeleteConfirmation.js
import React from 'react';
import { View, Text, Button } from 'react-native';

export default function DeleteConfirmation({
  visible,
  onCancel,
  onConfirm
}) {
  if (!visible) return null;

  return (
    <View accessibilityViewIsModal={true}>
      <Text>Delete this item?</Text>
      <Button title="Cancel" onPress={onCancel} />
      <Button title="Delete" onPress={onConfirm} />
    </View>
  );
}

15.2 Testing the Dialog Actions

test('calls the confirmation callback', async () => {
  const onCancel = jest.fn();
  const onConfirm = jest.fn();
  const user = userEvent.setup();

  render(
    <DeleteConfirmation
      visible
      onCancel={onCancel}
      onConfirm={onConfirm}
    />
  );

  expect(screen.getByText('Delete this item?'))
    .toBeOnTheScreen();

  await user.press(
    screen.getByRole('button', { name: 'Delete' })
  );

  expect(onConfirm).toHaveBeenCalledTimes(1);
});

16. Testing Components with Props and Rerendering

A component may change its output when its props change. React Native Testing Library provides rerendering utilities for checking the new output after supplying updated props.

// Greeting.test.js
test('updates the greeting when the name changes', () => {
  const { rerender } = render(<Greeting name="Asha" />);

  expect(screen.getByText('Hello, Asha!'))
    .toBeOnTheScreen();

  rerender(<Greeting name="Rahul" />);

  expect(screen.getByText('Hello, Rahul!'))
    .toBeOnTheScreen();

  expect(screen.queryByText('Hello, Asha!'))
    .not.toBeOnTheScreen();
});

This approach is useful for testing components that receive updated information from a parent, such as selected tabs, product details, user profiles, and loading flags.

17. Testing Accessibility

Accessibility testing checks whether UI elements expose meaningful roles, names, labels, and states. Accessible components are easier to use with assistive technologies and easier to identify in automated tests.

17.1 Accessibility Best Practices

  • Give buttons clear and descriptive labels.
  • Provide accessible labels for inputs that need them.
  • Use appropriate accessibility roles for custom interactive elements.
  • Expose relevant states, such as disabled or selected, where supported.
  • Avoid using visual text alone when a control needs a distinct accessible name.
  • Prefer accessibility-based queries in tests.

17.2 Example

import React from 'react';
import { Pressable, Text } from 'react-native';

export default function FavoriteButton({ onPress }) {
  return (
    <Pressable
      accessibilityRole="button"
      accessibilityLabel="Add to favorites"
      onPress={onPress}
    >
      <Text>Favorite</Text>
    </Pressable>
  );
}
test('exposes a descriptive accessible name', () => {
  render(<FavoriteButton onPress={() => {}} />);

  expect(
    screen.getByRole('button', { name: 'Add to favorites' })
  ).toBeOnTheScreen();
});

Component tests do not replace manual accessibility checks or testing with screen readers on actual devices.

18. Snapshot Testing

Snapshot testing records a representation of rendered output so it can be compared in later test runs. It can help detect unexpected changes to a component's structure, but it should not be the only form of testing.

18.1 Example Snapshot Test

import React from 'react';
import { render, screen } from '@testing-library/react-native';
import WelcomeMessage from './WelcomeMessage';

test('matches the welcome component snapshot', () => {
  render(<WelcomeMessage />);

  expect(screen.toJSON()).toMatchSnapshot();
});

Depending on the installed RNTL version, snapshot utilities and APIs may differ. Use the documented method for your version.

18.2 Advantages

  • Can reveal unexpected structural changes.
  • Can be useful for stable, presentational components.
  • Helps review changes in rendered output.

18.3 Limitations

  • A snapshot does not prove that a component behaves correctly.
  • Large snapshots can be difficult to review.
  • Updating snapshots without reviewing the changes may hide bugs.
  • Snapshots may become brittle when markup-like output changes frequently.

Prefer behavior-focused tests for important user actions, and use snapshots selectively.

19. Mocking Functions and Dependencies

Mocks replace real dependencies with controlled test implementations. They are useful for API calls, analytics functions, navigation callbacks, and other external behavior that should not run during a component test.

19.1 Mock Function with jest.fn()

const onSave = jest.fn();

onSave('profile');

expect(onSave).toHaveBeenCalledWith('profile');
expect(onSave).toHaveBeenCalledTimes(1);

19.2 Mocking a Module

jest.mock('./analytics', () => ({
  trackEvent: jest.fn()
}));

After triggering the behavior that uses the analytics module, verify that the mock was called with the expected event name and data.

19.3 Resetting Mock State

beforeEach(() => {
  jest.clearAllMocks();
});

Choose between clearing, resetting, or restoring mocks according to what each test needs. Clearing recorded calls does not necessarily remove an implementation configured with mockResolvedValue() or mockImplementation().

20. Testing Error Handling

Components should handle expected failures gracefully. Examples include failed network requests, invalid form data, missing content, and unsuccessful save operations.

20.1 What to Test

  • Whether an error message appears.
  • Whether a loading indicator disappears after failure.
  • Whether retry actions are available when required.
  • Whether the component avoids displaying misleading success messages.
  • Whether a failure leaves the UI in a consistent state.

20.2 Example Error Assertion

test('shows a message when the request fails', async () => {
  fetchUser.mockRejectedValue(new Error('Request failed'));

  render(<UserGreeting />);

  expect(await screen.findByText('Unable to load user'))
    .toBeOnTheScreen();
});

Ensure the module mock is configured correctly and reset between tests. The expected message must match the component's actual error-handling behavior.

21. Organizing Test Files

A clear testing structure makes a project easier to maintain as the number of components grows.

src/
  components/
    Button/
      Button.js
      Button.test.js
    TextInput/
      TextInput.js
      TextInput.test.js
    ProductCard/
      ProductCard.js
      ProductCard.test.js
  screens/
    LoginScreen/
      LoginScreen.js
      LoginScreen.test.js
  services/
    userApi.js

21.1 Naming Test Cases

Test names should describe observable behavior clearly.

  • Good: shows an error when the email is invalid
  • Good: calls the save callback when the user presses Save
  • Less useful: test button
  • Less useful: works correctly

21.2 Keep Tests Focused

Each test should verify one main behavior. A test can contain several related assertions when they describe the same scenario, but avoid combining unrelated workflows into a single large test.

22. Common Testing Mistakes and Troubleshooting

Problem Possible cause Solution
Element cannot be found Incorrect query text, missing render, or element not yet displayed. Check the rendered output, query, and component conditions. Use findBy... for asynchronous content.
Multiple elements match Several elements share the same text or role. Use a more specific accessible name or scope the query to a relevant section.
Test fails after state update Assertion runs before an asynchronous update completes. Use an appropriate asynchronous query or wait for the expected outcome.
Callback is not called Interaction targets the wrong element or the control is disabled. Verify the component's event handler and query the intended interactive element.
Module cannot be found Incorrect import path or mock path. Check relative paths, filenames, and module configuration.
Native module error A dependency expects native functionality unavailable in the test environment. Mock the relevant module according to the library's testing guidance.
Tests fail after package upgrades Version compatibility or changed APIs. Check React, React Native, Jest, and RNTL compatibility and update the test setup.
Snapshot changes unexpectedly Intentional or unintended rendered output changes. Review the diff and update the snapshot only when the change is expected.

22.1 Debugging Rendered Output

React Native Testing Library provides debugging utilities, such as screen.debug(), in supported versions. These can help inspect the rendered component tree when a query does not find the expected element.

render(<WelcomeMessage />);

screen.debug();

22.2 Avoid Overusing waitFor

Do not use waitFor() to hide incorrect queries or arbitrary timing problems. Prefer findBy... when waiting for an element to appear, and ensure that the component's asynchronous logic is correctly implemented.

22.3 Keep Test Setup Consistent

Shared setup can configure mocks, environment variables, and cleanup. However, global mocks should be limited to what the project genuinely needs so that tests remain predictable.

23. Component Testing Best Practices

  • Test user-visible behavior: Focus on what a user sees and does.
  • Prefer accessible queries: Use roles and accessible names wherever possible.
  • Test important states: Include loading, success, empty, invalid, and error states when relevant.
  • Test edge cases: Include empty values, long text, missing data, and disabled controls.
  • Mock external services: Keep component tests independent of real servers.
  • Keep tests deterministic: Avoid relying on real network requests or uncontrolled time.
  • Use meaningful test names: Describe the expected user-visible behavior.
  • Avoid implementation details: Do not make internal state or component structure the main focus of tests.
  • Review snapshots: Never update snapshots blindly.
  • Maintain dependency compatibility: Verify the supported React, React Native, Jest, and RNTL versions.
  • Run tests regularly: Execute tests during development and in continuous integration.
  • Complement component tests: Use device or end-to-end testing for native behavior that a JavaScript test environment cannot validate.

24. Understanding the Limitations of Component Tests

React Native Testing Library tests run in a JavaScript testing environment. They can verify component rendering and behavior, but they do not fully reproduce the operating system or the native implementations behind React Native components.

For example, a component test may verify that a camera permission message appears, but it does not prove that camera permissions work correctly on a physical device. Likewise, it cannot guarantee that all gestures, keyboard behaviors, native animations, or platform-specific integrations work perfectly.

Testing approach What it verifies
Unit tests Small functions and isolated logic.
Component tests Component rendering and user interactions in the JavaScript environment.
Integration tests How multiple components or services work together.
End-to-end tests Application workflows running in a more realistic app environment.
Manual device testing Real platform behavior, usability, and device-specific issues.

A reliable testing strategy combines these approaches according to the application's complexity and risk.

25. Practical Project: Test a Login Form

In this exercise, create a simple login form and test its initial display, validation, and successful submission.

25.1 Component Requirements

  • Display an email input.
  • Display a password input.
  • Display a Login button.
  • Show a validation message when required fields are empty.
  • Call a submission callback when valid input is submitted.

25.2 Example Login Component

// LoginForm.js
import React, { useState } from 'react';
import { View, Text, TextInput, Button } from 'react-native';

export default function LoginForm({ onLogin }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleLogin = () => {
    if (!email.trim() || !password) {
      setError('Email and password are required');
      return;
    }

    setError('');
    onLogin({ email: email.trim(), password });
  };

  return (
    <View>
      <TextInput
        accessibilityLabel="Email"
        placeholder="Enter email"
        value={email}
        onChangeText={setEmail}
        autoCapitalize="none"
        keyboardType="email-address"
      />
      <TextInput
        accessibilityLabel="Password"
        placeholder="Enter password"
        value={password}
        onChangeText={setPassword}
        secureTextEntry
      />
      <Button title="Login" onPress={handleLogin} />
      {error !== '' && <Text>{error}</Text>}
    </View>
  );
}

25.3 Test Empty Form Validation

// LoginForm.test.js
import React from 'react';
import { render, screen, userEvent } from '@testing-library/react-native';
import LoginForm from './LoginForm';

test('shows a validation message for empty fields', async () => {
  const onLogin = jest.fn();
  const user = userEvent.setup();

  render(<LoginForm onLogin={onLogin} />);

  await user.press(
    screen.getByRole('button', { name: 'Login' })
  );

  expect(
    screen.getByText('Email and password are required')
  ).toBeOnTheScreen();

  expect(onLogin).not.toHaveBeenCalled();
});

25.4 Test Successful Submission

test('submits the email and password', async () => {
  const onLogin = jest.fn();
  const user = userEvent.setup();

  render(<LoginForm onLogin={onLogin} />);

  await user.type(
    screen.getByLabelText('Email'),
    '[email protected]'
  );

  await user.type(
    screen.getByLabelText('Password'),
    'ExamplePassword123'
  );

  await user.press(
    screen.getByRole('button', { name: 'Login' })
  );

  expect(onLogin).toHaveBeenCalledWith({
    email: '[email protected]',
    password: 'ExamplePassword123'
  });
});

Project outcome: You will have a testable login component and a set of tests covering required-field validation and successful submission. In a real application, authentication itself should be handled by a service and tested separately with appropriate mocks and integration tests.

26. Practice Exercises

  1. Create a reusable button component and test that its callback is called when pressed.
  2. Create a search input and test that the entered text is submitted correctly.
  3. Create a password field and test that the required-field validation message appears.
  4. Create a profile card and test the displayed name and email.
  5. Create a product list and test that all supplied product names are visible.
  6. Create a loading component and test its loading and error states.
  7. Create a modal with Cancel and Confirm actions and test both callbacks.
  8. Create a favorite button with an accessible name and test its press behavior.
  9. Mock an API request and test both a successful response and a rejected request.
  10. Choose one component in your own project and write tests for its normal, empty, and error states.

27. Interview Questions and Answers

Q1. What is React Native Testing Library?

React Native Testing Library is a utility library for testing React Native components by rendering their output, querying elements, simulating interactions, and checking expected behavior.

Q2. Why is component testing important?

Component testing helps detect bugs, prevent regressions, validate user interactions, and improve confidence when maintaining or refactoring an application.

Q3. What is the role of Jest?

Jest runs test suites, provides assertions, creates mock functions, supports timers, and reports the test results.

Q4. What does render() do?

render() renders a React Native component in the test environment so that its output can be queried and tested.

Q5. What is the purpose of screen?

screen provides queries and utilities for interacting with the most recently rendered UI in supported versions of React Native Testing Library.

Q6. What is the difference between getBy and queryBy?

getBy... throws when a matching element is not found. queryBy... returns null when no matching element exists, making it useful for testing that content is absent.

Q7. When should findBy be used?

Use findBy... when an element is expected to appear after asynchronous work, such as an API response or a state update.

Q8. What is userEvent?

userEvent provides utilities for simulating common user interactions, such as pressing a button or typing into an input.

Q9. What is fireEvent?

fireEvent triggers a supported event on an element or an eligible event handler in the component tree. It is useful for events that are not covered by user-event utilities.

Q10. What is a mock function?

A mock function, commonly created using jest.fn(), records calls and can be configured to return values or simulate errors.

Q11. How do you test a button?

Render the component, locate the button using an accessible role and name, simulate a press, and assert the expected visible result or callback invocation.

Q12. How do you test an input field?

Locate the input by its accessible label or placeholder, simulate typing, submit or trigger the relevant action, and verify the expected output.

Q13. What is snapshot testing?

Snapshot testing stores a representation of rendered output and compares it in later test runs to identify unexpected changes. It should complement behavior-focused tests.

Q14. Why should tests avoid implementation details?

Tests based on internal state or component structure can break during harmless refactoring. Tests focused on user-visible behavior are generally more maintainable.

Q15. Can component tests guarantee that an app works on every device?

No. Component tests run in a JavaScript environment and do not fully exercise native platform code. Device, integration, and end-to-end testing are also important.

28. Quick Revision Checklist

  • Understand the purpose of component testing.
  • Install and configure compatible testing dependencies.
  • Know how to render a component using render().
  • Use screen to find elements.
  • Prefer accessibility roles, names, and labels.
  • Understand the difference between getBy, queryBy, and findBy.
  • Test buttons, inputs, forms, lists, modals, and conditional states.
  • Use userEvent for common user interactions where supported.
  • Use mocks to isolate API calls and external dependencies.
  • Test asynchronous behavior with suitable queries and utilities.
  • Keep tests focused, descriptive, and deterministic.
  • Review snapshots instead of updating them blindly.
  • Use device or end-to-end tests for native functionality that component tests cannot verify.

29. Conclusion

React Native Testing Library helps developers test components by focusing on the interface and behavior that users experience. By combining rendering utilities, accessible queries, user interactions, asynchronous testing, and Jest assertions, developers can create reliable tests for buttons, forms, lists, modals, and API-driven screens.

The most effective component tests are clear, focused, and based on observable behavior. Start with essential user workflows, cover important success and failure states, and expand the test suite as the application grows. Combine component tests with integration, end-to-end, and real-device testing to build greater confidence in a React Native application.

Learn more: Explore React Native Training | Register for a Course Demo | Visit JustAcademy

whatsapp